import re
import shlex
import getpass
import subprocess as sp

from PySide2.QtCore import QRect, QSize, Qt
from PySide2.QtGui import QFont, QIntValidator, QDoubleValidator
from PySide2.QtWidgets import *

btn_style_sheet = \
"""
QPushButton {
    background-color: rgb(0, 125, 196);
    color: rgb(255, 255, 255);
    border: 2px groove gray;
    border-radius: 2px;
    padding:2px 4px;
}
QPushButton:hover {
    background-color: rgb(170, 110, 0);
}
"""

UNFOLD_CHAR = u"\u25BC"
FOLD_CHAR = u"\u25B2"

CAMERA_PARAMS_MAP = {
    "Pixel Format": "PixelFormat",  # Note: mapping

    "Width": "Width",
    "Height": "Height",
    "Offset X": "OffsetX",
    "Offset Y": "OffsetY",
    "Decimation Horizontal": "DecimationHorizontal",
    "Decimation Vertical": "DecimationVertical",

    "Gain Selector": "GainSelector",
    "Gain Auto": "GainAuto",
    "Gain Auto Balance": "GainAutoBalance",
    "Gain": ["Gain", "GainRaw"],
    "Black Level Selector": "BlackLevelSelector",
    "Black Level Auto": "BlackLevelAuto",
    "Black Level": "BlackLevelEnabled",
    "Gamma Selector": "GammaSelector",
    "Gamma": "Gamma",

    "Exposure Mode": "ExposureMode",
    "Exposure Auto": "ExposureAuto",
    "Exposure Time Selector": "ExposureTimeSelector",
    "Exposure Time": ["ExposureTime", "ExposureTimeAbs"], 

    "Balance Ratio Selector": "BalanceRatioSelector",
    "Balance White Auto": "BalanceWhiteAuto",
    "Balance Ratio": ["BalanceRatio", "BalanceRatioAbs"],

    "Acquisition Mode": "TriggerMode", # Note: Continuous == Off, else == On
    "Trigger Selector": "TriggerSelector",
    "Trigger Source": "TriggerSource",  # Note: Trigger Source Number
    "Trigger Activation": "TriggerActivation",
    "Trigger Delay": ["TriggerDelay", "TriggerDelayAbs"],
    "Trigger Divider": "TriggerDivider",
    "Trigger Multiplier": "TriggerMultiplier",
    "Trigger Overlap": "TriggerOverlap",

    "Frame Rate": ["AcquisitionFrameRate", "AcquisitionFrameRateAbs"],
    "Packet Delay": "GevSCPD",
    "Packet Size": "GevSCPSPacketSize",
    "Throughput Limit": "DeviceLinkThroughputLimit",
}

PIXEL_FORMAT_MAP = {
    "Mono8": "mono8",
    "YCbCr411_8": "ycbcr411_8",
    "YUV422_8": "ycbcr422_8",
    "YUV422_YUYV_Packed": "ycbcr422_8",
    "YCbCr422_8": "ycbcr422_8",
    "BayerBG8": "bayerbggr",
    "BayerRG8": "bayerrggb",
    "BayerGR8": "bayergrbg",
    "BayerGB8": "bayergbrg",
    "RGB8": "rgb8",
    "RGB8Packed": "rgb8",
    "BGR8": "bgr8",
    "BGR8Packed": "bgr8",
}


class CustomComboBox(QComboBox):

    def __init__(self, scroll_wgt=None, *args, **kwargs):
        super(CustomComboBox, self).__init__(*args, **kwargs)  
        self.scroll_wgt = scroll_wgt
        self.setFocusPolicy(Qt.StrongFocus)

    def wheelEvent(self, event):
        if self.hasFocus():
            return QComboBox.wheelEvent(self, event)
        else:
            return event.ignore()


class CameraTuner(QDialog):
    
    def __init__(self, parent=None, output_wgt=None, host_pwd=None):
        super(CameraTuner, self).__init__(parent=parent)
        self.output_wgt = output_wgt
        self.host_pwd = host_pwd
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle("Camera Tuner")
        self.resize(560, 580)
        self.grid_lyt_global = QGridLayout(self)

        self.all_categories_label = []
        self.all_parameters_label = []
        self.all_parameters_wgt = []

        self.init_top()
        self.init_central()
        self.init_bottom()
        self.set_categories_label_style()
        self.set_parameters_label_style()
        self.set_labels_tooltip()

        self.filtered_parameters_label = self.all_parameters_label
        self.filtered_parameters_wgt = self.all_parameters_wgt
        for param_label, param_wgt in zip(self.filtered_parameters_label, self.filtered_parameters_wgt):
            if param_label.text() == "Trigger Source Number":
                self.filtered_parameters_label.remove(param_label)
                self.filtered_parameters_wgt.remove(param_wgt)

        self.slots_collector()
    
    def init_top(self):
        self.lbl_camera_params = QLabel(self)
        self.lbl_camera_params.setText("Camera Parameters")
        font = QFont()
        font.setPointSize(16)
        self.lbl_camera_params.setFont(font)
        self.lbl_camera_params.setAlignment(Qt.AlignCenter)
        self.grid_lyt_global.addWidget(self.lbl_camera_params, 0, 0, 1, 11)

    def init_central(self):
        self.scroll_area = QScrollArea(self)
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area_wgt = QWidget()
        self.scroll_area_wgt.setGeometry(QRect(0, 0, 566, 564))
        self.grid_lyt_scroll_area = QGridLayout(self.scroll_area_wgt)
        self.scroll_area.setWidget(self.scroll_area_wgt)
        self.grid_lyt_global.addWidget(self.scroll_area, 1, 0, 1, 11)

        # ---------------------------- Basic ---------------------------- #
        self.btn_basic = QPushButton(self.scroll_area_wgt)
        self.btn_basic.setText("Basic")
        self.btn_basic.setStyleSheet("border: none")
        font = QFont()
        font.setPointSize(14)
        self.btn_basic.setFont(font)
        self.btn_basic.setDefault(False)
        self.btn_basic.setAutoDefault(False)
        self.grid_lyt_scroll_area.addWidget(self.btn_basic, 0, 0, 1, 1)

        self.wgt_basic = QWidget(self.scroll_area_wgt)
        self.grid_lyt_wgt_basic = QGridLayout(self.wgt_basic)
        self.grid_lyt_scroll_area.addWidget(self.wgt_basic, 1, 0, 1, 1)

        self.init_basic_common(index=0)
        self.init_basic_roi(index=1)

        # ---------------------------- Advanced ---------------------------- #
        self.btn_advanced = QPushButton(self.scroll_area_wgt)
        self.btn_advanced.setText(UNFOLD_CHAR + "  Advanced")
        self.btn_advanced.setStyleSheet("border: none")
        font = QFont()
        font.setPointSize(14)
        self.btn_advanced.setFont(font)
        self.btn_advanced.setDefault(False)
        self.btn_advanced.setAutoDefault(False)
        self.grid_lyt_scroll_area.addWidget(self.btn_advanced, 2, 0, 1, 1)

        self.wgt_advanced = QWidget(self.scroll_area_wgt)
        self.wgt_advanced.hide()
        self.grid_lyt_wgt_advanced = QGridLayout(self.wgt_advanced)
        self.grid_lyt_scroll_area.addWidget(self.wgt_advanced, 3, 0, 1, 1)

        self.init_advanced_analog_control(index=0)
        self.init_advanced_exposure(index=1)
        self.init_advanced_balance(index=2)
        self.init_advanced_acquisition_trigger(index=3)
        self.init_advanced_binning(index=4)
        self.init_advanced_transmission(index=5)
    
    def init_bottom(self):
        self.btn_ipconfig = QPushButton(self)
        self.btn_ipconfig.setText("IPConfig")
        self.btn_ipconfig.setDefault(False)
        self.btn_ipconfig.setAutoDefault(False)
        self.btn_ipconfig.setStyleSheet(btn_style_sheet)
        self.btn_ipconfig.setMinimumSize(QSize(80, 0))
        self.btn_ipconfig.setMaximumSize(QSize(80, 16777215))
        self.grid_lyt_global.addWidget(self.btn_ipconfig, 2, 1, 1, 1)

        self.btn_propview = QPushButton(self)
        self.btn_propview.setText("PropView")
        self.btn_propview.setDefault(False)
        self.btn_propview.setAutoDefault(False)
        self.btn_propview.setStyleSheet(btn_style_sheet)
        self.btn_propview.setMinimumSize(QSize(80, 0))
        self.btn_propview.setMaximumSize(QSize(80, 16777215))
        self.grid_lyt_global.addWidget(self.btn_propview, 2, 3, 1, 1)
        
        self.btn_import = QPushButton(self)
        self.btn_import.setText("Import")
        self.btn_import.setDefault(False)
        self.btn_import.setAutoDefault(False)
        self.btn_import.setStyleSheet(btn_style_sheet)
        self.btn_import.setMinimumSize(QSize(80, 0))
        self.btn_import.setMaximumSize(QSize(80, 16777215))
        self.grid_lyt_global.addWidget(self.btn_import, 2, 5, 1, 1)

        self.btn_tune = QPushButton(self)
        self.btn_tune.setText("Tune")
        self.btn_tune.setDefault(False)
        self.btn_tune.setAutoDefault(False)
        self.btn_tune.setStyleSheet(btn_style_sheet)
        self.btn_tune.setMinimumSize(QSize(80, 0))
        self.btn_tune.setMaximumSize(QSize(80, 16777215))
        self.grid_lyt_global.addWidget(self.btn_tune, 2, 7, 1, 1)

        self.btn_apply = QPushButton(self)
        self.btn_apply.setText("Apply")
        self.btn_apply.setDefault(False)
        self.btn_apply.setAutoDefault(False)
        self.btn_apply.setStyleSheet(btn_style_sheet)
        self.btn_apply.setMinimumSize(QSize(80, 0))
        self.btn_apply.setMaximumSize(QSize(80, 16777215))
        self.grid_lyt_global.addWidget(self.btn_apply, 2, 9, 1, 1)
        
        spacer1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.grid_lyt_global.addItem(spacer1, 2, 0, 1, 1)
        spacer2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.grid_lyt_global.addItem(spacer2, 2, 2, 1, 1)
        spacer3 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.grid_lyt_global.addItem(spacer3, 2, 4, 1, 1)
        spacer4 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.grid_lyt_global.addItem(spacer4, 2, 6, 1, 1)
        spacer5 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.grid_lyt_global.addItem(spacer5, 2, 8, 1, 1)
        spacer6 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.grid_lyt_global.addItem(spacer6, 2, 10, 1, 1)
    
    def init_basic_common(self, index):
        # ---------------------------- Basic: Device ---------------------------- #
        # Common label
        self.title_common = QLabel(self.wgt_basic)
        self.title_common.setText("Common")
        self.grid_lyt_wgt_basic.addWidget(self.title_common, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_common)

        # Common widget
        self.wgt_common = QWidget(self.wgt_basic)
        form_lyt_device = QFormLayout(self.wgt_common)
        self.grid_lyt_wgt_basic.addWidget(self.wgt_common, 2 * index + 1, 0, 1, 1)

        # Common widget: Serial
        self.lbl_serial = QLabel(self.wgt_common)
        self.lbl_serial.setText("Serial")
        form_lyt_device.setWidget(1, QFormLayout.LabelRole, self.lbl_serial)
        self.le_serial = QLineEdit(self.wgt_common)
        self.le_serial.setPlaceholderText("Type: string")
        form_lyt_device.setWidget(1, QFormLayout.FieldRole, self.le_serial)
        self.all_parameters_label.append(self.lbl_serial)
        self.all_parameters_wgt.append(self.le_serial)

        # Common widget: Pixel Format
        self.lbl_pixel_format = QLabel(self.wgt_common)
        self.lbl_pixel_format.setText("Pixel Format")
        form_lyt_device.setWidget(2, QFormLayout.LabelRole, self.lbl_pixel_format)
        self.cbb_pixel_format = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_pixel_format.addItems([
            "mono8", "ycbcr411_8", "ycbcr422_8", "rgb8", "bgr8",
            "bayerbggr", "bayerrggb", "bayergrbg", "bayergbrg",
        ])
        form_lyt_device.setWidget(2, QFormLayout.FieldRole, self.cbb_pixel_format)
        self.all_parameters_label.append(self.lbl_pixel_format)
        self.all_parameters_wgt.append(self.cbb_pixel_format)

        # Common widget: Exposure Time
        self.lbl_exposure_time = QLabel(self.wgt_common)
        self.lbl_exposure_time.setText("Exposure Time")
        form_lyt_device.setWidget(3, QFormLayout.LabelRole, self.lbl_exposure_time)
        self.le_exposure_time = QLineEdit(self.wgt_common)
        self.le_exposure_time.setValidator(QDoubleValidator(-1, 1e+07, 6))
        self.le_exposure_time.setPlaceholderText("Type: float;  Range: -1 - 1e+07")
        form_lyt_device.setWidget(3, QFormLayout.FieldRole, self.le_exposure_time)
        self.all_parameters_label.append(self.lbl_exposure_time)
        self.all_parameters_wgt.append(self.le_exposure_time)

        # Common widget: Frame Rate
        self.lbl_frame_rate = QLabel(self.wgt_common)
        self.lbl_frame_rate.setText("Frame Rate")
        form_lyt_device.setWidget(4, QFormLayout.LabelRole, self.lbl_frame_rate)
        self.le_frame_rate = QLineEdit(self.wgt_common)
        self.le_frame_rate.setValidator(QDoubleValidator(0, 120, 6))
        self.le_frame_rate.setPlaceholderText("Type: float;  Range: 0 - 120")
        form_lyt_device.setWidget(4, QFormLayout.FieldRole, self.le_frame_rate)
        self.all_parameters_label.append(self.lbl_frame_rate)
        self.all_parameters_wgt.append(self.le_frame_rate)

    def init_basic_roi(self, index):
        # ---------------------------- Basic: ROI ---------------------------- #
        # ROI label
        self.title_roi = QLabel(self.wgt_basic)
        self.title_roi.setText("ROI")
        self.grid_lyt_wgt_basic.addWidget(self.title_roi, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_roi)

        # Device ROI
        self.wgt_roi = QWidget(self.wgt_basic)
        form_lyt_roi = QFormLayout(self.wgt_roi)
        self.grid_lyt_wgt_basic.addWidget(self.wgt_roi, 2 * index + 1, 0, 1, 1)

        # ROI widget: Width
        self.lbl_width = QLabel(self.wgt_roi)
        self.lbl_width.setText("Width")
        form_lyt_roi.setWidget(1, QFormLayout.LabelRole, self.lbl_width)
        self.le_width = QLineEdit(self.wgt_roi)
        self.le_width.setValidator(QIntValidator(0, 2147483647))
        self.le_width.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_roi.setWidget(1, QFormLayout.FieldRole, self.le_width)
        self.all_parameters_label.append(self.lbl_width)
        self.all_parameters_wgt.append(self.le_width)

        # ROI widget: Height
        self.lbl_height = QLabel(self.wgt_roi)
        self.lbl_height.setText("Height")
        form_lyt_roi.setWidget(2, QFormLayout.LabelRole, self.lbl_height)
        self.le_height = QLineEdit(self.wgt_roi)
        self.le_height.setValidator(QIntValidator(0, 2147483647))
        self.le_height.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_roi.setWidget(2, QFormLayout.FieldRole, self.le_height)
        self.all_parameters_label.append(self.lbl_height)
        self.all_parameters_wgt.append(self.le_height)

        # ROI widget: Offset X
        self.lbl_offset_x = QLabel(self.wgt_roi)
        self.lbl_offset_x.setText("Offset X")
        form_lyt_roi.setWidget(3, QFormLayout.LabelRole, self.lbl_offset_x)
        self.le_offset_x = QLineEdit(self.wgt_roi)
        self.le_offset_x.setValidator(QIntValidator(0, 2147483647))
        self.le_offset_x.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_roi.setWidget(3, QFormLayout.FieldRole, self.le_offset_x)
        self.all_parameters_label.append(self.lbl_offset_x)
        self.all_parameters_wgt.append(self.le_offset_x)

        # ROI widget: Offset Y
        self.lbl_offset_y = QLabel(self.wgt_roi)
        self.lbl_offset_y.setText("Offset Y")
        form_lyt_roi.setWidget(4, QFormLayout.LabelRole, self.lbl_offset_y)
        self.le_offset_y = QLineEdit(self.wgt_roi)
        self.le_offset_y.setValidator(QIntValidator(0, 2147483647))
        self.le_offset_y.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_roi.setWidget(4, QFormLayout.FieldRole, self.le_offset_y)
        self.all_parameters_label.append(self.lbl_offset_y)
        self.all_parameters_wgt.append(self.le_offset_y)

        # ROI widget: Decimation Horizontal
        self.lbl_decimation_horizontal = QLabel(self.wgt_roi)
        self.lbl_decimation_horizontal.setText("Decimation Horizontal")
        form_lyt_roi.setWidget(5, QFormLayout.LabelRole, self.lbl_decimation_horizontal)
        self.le_decimation_horizontal = QLineEdit(self.wgt_roi)
        self.le_decimation_horizontal.setValidator(QIntValidator(0, 2147483647))
        self.le_decimation_horizontal.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_roi.setWidget(5, QFormLayout.FieldRole, self.le_decimation_horizontal)
        self.all_parameters_label.append(self.lbl_decimation_horizontal)
        self.all_parameters_wgt.append(self.le_decimation_horizontal)

        # ROI widget: Decimation Vertical
        self.lbl_decimation_vertical = QLabel(self.wgt_roi)
        self.lbl_decimation_vertical.setText("Decimation Vertical")
        form_lyt_roi.setWidget(6, QFormLayout.LabelRole, self.lbl_decimation_vertical)
        self.le_decimation_vertical = QLineEdit(self.wgt_roi)
        self.le_decimation_vertical.setValidator(QIntValidator(0, 2147483647))
        self.le_decimation_vertical.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_roi.setWidget(6, QFormLayout.FieldRole, self.le_decimation_vertical)
        self.all_parameters_label.append(self.lbl_decimation_vertical)
        self.all_parameters_wgt.append(self.le_decimation_vertical)

    def init_advanced_analog_control(self, index):
        # ---------------------------- Advanced: Analog Control ---------------------------- #
        # Analog Control label
        self.title_analog_control = QLabel(self.wgt_advanced)
        self.title_analog_control.setText("Analog Control")
        self.grid_lyt_wgt_advanced.addWidget(self.title_analog_control, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_analog_control)

        # Analog Control widget
        self.wgt_analog_control = QWidget(self.wgt_advanced)
        form_lyt_analog_control = QFormLayout(self.wgt_analog_control)
        self.grid_lyt_wgt_advanced.addWidget(self.wgt_analog_control, 2 * index + 1, 0, 1, 1)

        # Analog Control widget: Gain Selector
        self.lbl_gain_selector = QLabel(self.wgt_analog_control)
        self.lbl_gain_selector.setText("Gain Selector")
        form_lyt_analog_control.setWidget(1, QFormLayout.LabelRole, self.lbl_gain_selector)
        self.cbb_gain_selector = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_gain_selector.addItems([
            "(default)",
            "All", "Red", "Green", "Blue", "Y", "U", "V",
        ])
        form_lyt_analog_control.setWidget(1, QFormLayout.FieldRole, self.cbb_gain_selector)
        self.all_parameters_label.append(self.lbl_gain_selector)
        self.all_parameters_wgt.append(self.cbb_gain_selector)
        
        # Analog Control widget: Gain Auto
        self.lbl_gain_auto = QLabel(self.wgt_analog_control)
        self.lbl_gain_auto.setText("Gain Auto")
        form_lyt_analog_control.setWidget(2, QFormLayout.LabelRole, self.lbl_gain_auto)
        self.cbb_gain_auto = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_gain_auto.addItems([
            "(default)",
            "Off", "Once", "Continuous",
        ])
        form_lyt_analog_control.setWidget(2, QFormLayout.FieldRole, self.cbb_gain_auto)
        self.all_parameters_label.append(self.lbl_gain_auto)
        self.all_parameters_wgt.append(self.cbb_gain_auto)

        # Analog Control widget: Gain Auto Balance
        self.lbl_gain_auto_balance = QLabel(self.wgt_analog_control)
        self.lbl_gain_auto_balance.setText("Gain Auto Balance")
        form_lyt_analog_control.setWidget(3, QFormLayout.LabelRole, self.lbl_gain_auto_balance)
        self.cbb_gain_auto_balance = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_gain_auto_balance.addItems([
            "(default)",
            "Off", "Once", "Continuous",
        ])
        form_lyt_analog_control.setWidget(3, QFormLayout.FieldRole, self.cbb_gain_auto_balance)
        self.all_parameters_label.append(self.lbl_gain_auto_balance)
        self.all_parameters_wgt.append(self.cbb_gain_auto_balance)

        # Analog Control widget: Gain
        self.lbl_gain = QLabel(self.wgt_analog_control)
        self.lbl_gain.setText("Gain")
        form_lyt_analog_control.setWidget(4, QFormLayout.LabelRole, self.lbl_gain)
        self.le_gain = QLineEdit(self.wgt_analog_control)
        self.le_gain.setValidator(QDoubleValidator(-9999, 9999, 6))
        self.le_gain.setPlaceholderText("Type: float;  Range: -9999 - 9999")
        form_lyt_analog_control.setWidget(4, QFormLayout.FieldRole, self.le_gain)
        self.all_parameters_label.append(self.lbl_gain)
        self.all_parameters_wgt.append(self.le_gain)

        # Analog Control widget: Black Level Selector
        self.lbl_black_level_selector = QLabel(self.wgt_analog_control)
        self.lbl_black_level_selector.setText("Black Level Selector")
        form_lyt_analog_control.setWidget(5, QFormLayout.LabelRole, self.lbl_black_level_selector)
        self.cbb_black_level_selector = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_black_level_selector.addItems([
            "(default)",
            "All", "Red", "Green", "Blue", "Y", "U", "V", "Tap1", "Tap2",
        ])
        form_lyt_analog_control.setWidget(5, QFormLayout.FieldRole, self.cbb_black_level_selector)
        self.all_parameters_label.append(self.lbl_black_level_selector)
        self.all_parameters_wgt.append(self.cbb_black_level_selector)
        
        # Analog Control widget: Black Level Auto
        self.lbl_black_level_auto = QLabel(self.wgt_analog_control)
        self.lbl_black_level_auto.setText("Black Level Auto")
        form_lyt_analog_control.setWidget(6, QFormLayout.LabelRole, self.lbl_black_level_auto)
        self.cbb_black_level_auto = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_black_level_auto.addItems([
            "(default)",
            "Off", "Once", "Continuous",
        ])
        form_lyt_analog_control.setWidget(6, QFormLayout.FieldRole, self.cbb_black_level_auto)
        self.all_parameters_label.append(self.lbl_black_level_auto)
        self.all_parameters_wgt.append(self.cbb_black_level_auto)
        
        # Analog Control widget: Black Level
        self.lbl_black_level = QLabel(self.wgt_analog_control)
        self.lbl_black_level.setText("Black Level")
        form_lyt_analog_control.setWidget(7, QFormLayout.LabelRole, self.lbl_black_level)
        self.le_black_level = QLineEdit(self.wgt_analog_control)
        self.le_black_level.setValidator(QDoubleValidator(-9999, 9999, 6))
        self.le_black_level.setPlaceholderText("Type: float;  Range: -9999 - 9999")
        form_lyt_analog_control.setWidget(7, QFormLayout.FieldRole, self.le_black_level)
        self.all_parameters_label.append(self.lbl_black_level)
        self.all_parameters_wgt.append(self.le_black_level)

        # Analog Control widget: Gamma Selector
        self.lbl_gamma_selector = QLabel(self.wgt_analog_control)
        self.lbl_gamma_selector.setText("Gamma Selector")
        form_lyt_analog_control.setWidget(8, QFormLayout.LabelRole, self.lbl_gamma_selector)
        self.cbb_gamma_selector = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_gamma_selector.addItems([
            "(default)",
            "sRGB", "User",
        ])
        form_lyt_analog_control.setWidget(8, QFormLayout.FieldRole, self.cbb_gamma_selector)
        self.all_parameters_label.append(self.lbl_gamma_selector)
        self.all_parameters_wgt.append(self.cbb_gamma_selector)
        
        # Analog Control widget: Gamma
        self.lbl_gamma = QLabel(self.wgt_analog_control)
        self.lbl_gamma.setText("Gamma")
        form_lyt_analog_control.setWidget(9, QFormLayout.LabelRole, self.lbl_gamma)
        self.le_gamma = QLineEdit(self.wgt_analog_control)
        self.le_gamma.setValidator(QDoubleValidator(0, 5, 6))
        self.le_gamma.setPlaceholderText("Type: float;  Range: 0 - 5")
        form_lyt_analog_control.setWidget(9, QFormLayout.FieldRole, self.le_gamma)
        self.all_parameters_label.append(self.lbl_gamma)
        self.all_parameters_wgt.append(self.le_gamma)

    def init_advanced_exposure(self, index):
        # ---------------------------- Advanced: Exposure ---------------------------- #
        # Exposure label
        self.title_exposure = QLabel(self.wgt_advanced)
        self.title_exposure.setText("Exposure")
        self.grid_lyt_wgt_advanced.addWidget(self.title_exposure, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_exposure)

        # Exposure widget
        self.wgt_exposure = QWidget(self.wgt_advanced)
        form_lyt_exposure = QFormLayout(self.wgt_exposure)
        self.grid_lyt_wgt_advanced.addWidget(self.wgt_exposure, 2 * index + 1, 0, 1, 1)

        # Exposure widget: Exposure Mode
        self.lbl_exposure_mode = QLabel(self.wgt_exposure)
        self.lbl_exposure_mode.setText("Exposure Mode")
        form_lyt_exposure.setWidget(1, QFormLayout.LabelRole, self.lbl_exposure_mode)
        self.cbb_exposure_mode = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_exposure_mode.addItems([
            "(default)",
            "Off", "Timed", "Trigger-width", "Trigger-controlled",
        ])
        form_lyt_exposure.setWidget(1, QFormLayout.FieldRole, self.cbb_exposure_mode)
        self.all_parameters_label.append(self.lbl_exposure_mode)
        self.all_parameters_wgt.append(self.cbb_exposure_mode)
        
        # Exposure widget: Exposure Auto
        self.lbl_exposure_auto = QLabel(self.wgt_exposure)
        self.lbl_exposure_auto.setText("Exposure Auto")
        form_lyt_exposure.setWidget(2, QFormLayout.LabelRole, self.lbl_exposure_auto)
        self.cbb_exposure_auto = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_exposure_auto.addItems([
            "Off", 
            "Once", "Continuous",
        ])
        form_lyt_exposure.setWidget(2, QFormLayout.FieldRole, self.cbb_exposure_auto)
        self.all_parameters_label.append(self.lbl_exposure_auto)
        self.all_parameters_wgt.append(self.cbb_exposure_auto)

        # Exposure widget: Exposure Time Selector
        self.lbl_exposure_time_selector = QLabel(self.wgt_exposure)
        self.lbl_exposure_time_selector.setText("Exposure Time Selector")
        form_lyt_exposure.setWidget(3, QFormLayout.LabelRole, self.lbl_exposure_time_selector)
        self.cbb_exposure_time_selector = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_exposure_time_selector.addItems([
            "(default)",
            "Common", "Red", "Green", "Stage1",
        ])
        form_lyt_exposure.setWidget(3, QFormLayout.FieldRole, self.cbb_exposure_time_selector)
        self.all_parameters_label.append(self.lbl_exposure_time_selector)
        self.all_parameters_wgt.append(self.cbb_exposure_time_selector)

    def init_advanced_balance(self, index):
        # ---------------------------- Advanced: Balance ---------------------------- #
        # Balance label
        self.title_balance = QLabel(self.wgt_advanced)
        self.title_balance.setText("Balance")
        self.grid_lyt_wgt_advanced.addWidget(self.title_balance, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_balance)

        # Balance widget
        self.wgt_balance = QWidget(self.wgt_advanced)
        form_lyt_balance = QFormLayout(self.wgt_balance)
        self.grid_lyt_wgt_advanced.addWidget(self.wgt_balance, 2 * index + 1, 0, 1, 1)

        # Balance widget: Balance Ratio Selector
        self.lbl_balance_ratio_selector = QLabel(self.wgt_balance)
        self.lbl_balance_ratio_selector.setText("Balance Ratio Selector")
        form_lyt_balance.setWidget(1, QFormLayout.LabelRole, self.lbl_balance_ratio_selector)
        self.cbb_balance_ratio_selector = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_balance_ratio_selector.addItems([
            "(default)",
            "All", "Red", "Green", "Blue", "Y", "U", "V", "Tap1", "Tap2",
        ])
        form_lyt_balance.setWidget(1, QFormLayout.FieldRole, self.cbb_balance_ratio_selector)
        self.all_parameters_label.append(self.lbl_balance_ratio_selector)
        self.all_parameters_wgt.append(self.cbb_balance_ratio_selector)
        
        # Balance widget: Balance White Auto
        self.lbl_balance_white_auto = QLabel(self.wgt_balance)
        self.lbl_balance_white_auto.setText("Balance White Auto")
        form_lyt_balance.setWidget(2, QFormLayout.LabelRole, self.lbl_balance_white_auto)
        self.cbb_balance_white_auto = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_balance_white_auto.addItems([
            "(default)",
            "Off", "Once", "Continuous",
        ])
        form_lyt_balance.setWidget(2, QFormLayout.FieldRole, self.cbb_balance_white_auto)
        self.all_parameters_label.append(self.lbl_balance_white_auto)
        self.all_parameters_wgt.append(self.cbb_balance_white_auto)
        
        # Balance widget: Balance Ratio
        self.lbl_balance_ratio = QLabel(self.wgt_balance)
        self.lbl_balance_ratio.setText("Balance Ratio")
        form_lyt_balance.setWidget(3, QFormLayout.LabelRole, self.lbl_balance_ratio)
        self.le_balance_ratio = QLineEdit(self.wgt_balance)
        self.le_balance_ratio.setPlaceholderText("Type: float;  Range: 0 - 9999")
        form_lyt_balance.setWidget(3, QFormLayout.FieldRole, self.le_balance_ratio)
        self.all_parameters_label.append(self.lbl_balance_ratio)
        self.all_parameters_wgt.append(self.le_balance_ratio)

    def init_advanced_acquisition_trigger(self, index):
        # ---------------------------- Advanced: Acquisition Trigger ---------------------------- #
        # Acquisition Trigger label
        self.title_acquisition_trigger = QLabel(self.wgt_advanced)
        self.title_acquisition_trigger.setText("Acquisition Trigger")
        self.grid_lyt_wgt_advanced.addWidget(self.title_acquisition_trigger, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_acquisition_trigger)

        # Acquisition Trigger widget
        self.wgt_acquisition_trigger = QWidget(self.wgt_advanced)
        form_lyt_acquisition_trigger = QFormLayout(self.wgt_acquisition_trigger)
        self.grid_lyt_wgt_advanced.addWidget(self.wgt_acquisition_trigger, 2 * index + 1, 0, 1, 1)

        # Acquisition Trigger widget: Acquisition Mode
        self.lbl_acquisition_mode = QLabel(self.wgt_acquisition_trigger)
        self.lbl_acquisition_mode.setText("Acquisition Mode")
        form_lyt_acquisition_trigger.setWidget(1, QFormLayout.LabelRole, self.lbl_acquisition_mode)
        self.cbb_acquisition_mode = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_acquisition_mode.addItems([
            "(default)",
            "Continuous", "Multiframe", "Singleframe",
        ])
        form_lyt_acquisition_trigger.setWidget(1, QFormLayout.FieldRole, self.cbb_acquisition_mode)
        self.all_parameters_label.append(self.lbl_acquisition_mode)
        self.all_parameters_wgt.append(self.cbb_acquisition_mode)

        # Acquisition Trigger widget: Trigger Selector
        self.lbl_trigger_selector = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_selector.setText("Trigger Selector")
        form_lyt_acquisition_trigger.setWidget(2, QFormLayout.LabelRole, self.lbl_trigger_selector)
        self.cbb_trigger_selector = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_trigger_selector.addItems([
            "(default)",
            "AcquisitionStart", "AcquisitionEnd", "AcquisitionActive", 
            "FrameStart", "FrameEnd", "FrameActive",
            "FrameBurstStart", "FrameBurstEnd", "FrameBurstActive", 
            "LineStart", "ExposureStart", "ExposureEnd", "ExposureActive",
            "MultiSlopeExposureLimit1",
        ])
        form_lyt_acquisition_trigger.setWidget(2, QFormLayout.FieldRole, self.cbb_trigger_selector)
        self.all_parameters_label.append(self.lbl_trigger_selector)
        self.all_parameters_wgt.append(self.cbb_trigger_selector)

        # Acquisition Trigger widget: Trigger Source
        self.lbl_trigger_source = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_source.setText("Trigger Source")
        form_lyt_acquisition_trigger.setWidget(3, QFormLayout.LabelRole, self.lbl_trigger_source)
        self.cbb_trigger_source = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_trigger_source.addItems([
            "(default)",
            "Software", "SoftwareSignal<n>", "UserOutput<n>",
            "Line<n>", "Counter<n>Start", "Counter<n>End",
            "Timer<n>Start", "Timer<n>End", "Encoder<n>",
            "<LogicBlock<n>>", "Action<n>", "LinkTrigger<n>", "CC<n>",
        ])
        form_lyt_acquisition_trigger.setWidget(3, QFormLayout.FieldRole, self.cbb_trigger_source)
        self.all_parameters_label.append(self.lbl_trigger_source)
        self.all_parameters_wgt.append(self.cbb_trigger_source)

        # Acquisition Trigger widget: Trigger Source Number
        self.lbl_trigger_source_number = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_source_number.setText("Trigger Source Number")
        form_lyt_acquisition_trigger.setWidget(4, QFormLayout.LabelRole, self.lbl_trigger_source_number)
        self.le_trigger_source_number = QLineEdit(self.wgt_acquisition_trigger)
        self.le_trigger_source_number.setValidator(QIntValidator(0, 9999))
        self.le_trigger_source_number.setPlaceholderText("Type: int;  Range: 0 - 9999")
        form_lyt_acquisition_trigger.setWidget(4, QFormLayout.FieldRole, self.le_trigger_source_number)
        self.all_parameters_label.append(self.lbl_trigger_source_number)
        self.all_parameters_wgt.append(self.le_trigger_source_number)

        # Acquisition Trigger widget: Trigger Activation
        self.lbl_trigger_activation = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_activation.setText("Trigger Activation")
        form_lyt_acquisition_trigger.setWidget(5, QFormLayout.LabelRole, self.lbl_trigger_activation)
        self.cbb_trigger_activation = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_trigger_activation.addItems([
            "(default)",
            "RisingEdge", "FallingEdge", "AnyEdge", "LevelHigh", "LevelLow",
        ])
        form_lyt_acquisition_trigger.setWidget(5, QFormLayout.FieldRole, self.cbb_trigger_activation)
        self.all_parameters_label.append(self.lbl_trigger_activation)
        self.all_parameters_wgt.append(self.cbb_trigger_activation)

        # Acquisition Trigger widget: Trigger Delay
        self.lbl_trigger_delay = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_delay.setText("Trigger Delay")
        form_lyt_acquisition_trigger.setWidget(6, QFormLayout.LabelRole, self.lbl_trigger_delay)
        self.le_trigger_delay = QLineEdit(self.wgt_acquisition_trigger)
        self.le_trigger_delay.setValidator(QDoubleValidator(-1, 2.147484e+09, 6))
        self.le_trigger_delay.setPlaceholderText("Type: float;  Range: -1 - 2.147484e+09")
        form_lyt_acquisition_trigger.setWidget(6, QFormLayout.FieldRole, self.le_trigger_delay)
        self.all_parameters_label.append(self.lbl_trigger_delay)
        self.all_parameters_wgt.append(self.le_trigger_delay)

        # Acquisition Trigger widget: Trigger Divider
        self.lbl_trigger_divider = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_divider.setText("Trigger Divider")
        form_lyt_acquisition_trigger.setWidget(7, QFormLayout.LabelRole, self.lbl_trigger_divider)
        self.le_trigger_divider = QLineEdit(self.wgt_acquisition_trigger)
        self.le_trigger_divider.setValidator(QIntValidator(0, 2147483647))
        self.le_trigger_divider.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_acquisition_trigger.setWidget(7, QFormLayout.FieldRole, self.le_trigger_divider)
        self.all_parameters_label.append(self.lbl_trigger_divider)
        self.all_parameters_wgt.append(self.le_trigger_divider)

        # Acquisition Trigger widget: Trigger Multiplier
        self.lbl_trigger_multiplier = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_multiplier.setText("Trigger Multiplier")
        form_lyt_acquisition_trigger.setWidget(8, QFormLayout.LabelRole, self.lbl_trigger_multiplier)
        self.le_trigger_multiplier = QLineEdit(self.wgt_acquisition_trigger)
        self.le_trigger_multiplier.setValidator(QIntValidator(0, 2147483647))
        self.le_trigger_multiplier.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_acquisition_trigger.setWidget(8, QFormLayout.FieldRole, self.le_trigger_multiplier)
        self.all_parameters_label.append(self.lbl_trigger_multiplier)
        self.all_parameters_wgt.append(self.le_trigger_multiplier)

        # Acquisition Trigger widget: Trigger Overlap
        self.lbl_trigger_overlap = QLabel(self.wgt_acquisition_trigger)
        self.lbl_trigger_overlap.setText("Trigger Overlap")
        form_lyt_acquisition_trigger.setWidget(9, QFormLayout.LabelRole, self.lbl_trigger_overlap)
        self.cbb_trigger_overlap = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_trigger_overlap.addItems([
            "(default)",
            "Off", "ReadOut", "PreviousFrame", "PreviousLine",
        ])
        form_lyt_acquisition_trigger.setWidget(9, QFormLayout.FieldRole, self.cbb_trigger_overlap)
        self.all_parameters_label.append(self.lbl_trigger_overlap)
        self.all_parameters_wgt.append(self.cbb_trigger_overlap)

        # Acquisition Trigger widget: HW Trigger Timeout
        self.lbl_hw_trigger_timeout = QLabel(self.wgt_acquisition_trigger)
        self.lbl_hw_trigger_timeout.setText("HW Trigger Timeout")
        form_lyt_acquisition_trigger.setWidget(10, QFormLayout.LabelRole, self.lbl_hw_trigger_timeout)
        self.le_hw_trigger_timeout = QLineEdit(self.wgt_acquisition_trigger)
        self.le_hw_trigger_timeout.setValidator(QIntValidator(0, 2147483647))
        self.le_hw_trigger_timeout.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_acquisition_trigger.setWidget(10, QFormLayout.FieldRole, self.le_hw_trigger_timeout)
        self.all_parameters_label.append(self.lbl_hw_trigger_timeout)
        self.all_parameters_wgt.append(self.le_hw_trigger_timeout)

    def init_advanced_binning(self, index):
        # ---------------------------- Advanced: Binning ---------------------------- #
        # Binning label
        self.title_binning = QLabel(self.wgt_advanced)
        self.title_binning.setText("Binning")
        self.grid_lyt_wgt_advanced.addWidget(self.title_binning, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_binning)

        # Binning widget
        self.wgt_binning = QWidget(self.wgt_advanced)
        form_lyt_binning = QFormLayout(self.wgt_binning)
        self.grid_lyt_wgt_advanced.addWidget(self.wgt_binning, 2 * index + 1, 0, 1, 1)

        # Binning widget: Binning Selector
        self.lbl_binning_selector = QLabel(self.wgt_binning)
        self.lbl_binning_selector.setText("Binning Selector")
        form_lyt_binning.setWidget(1, QFormLayout.LabelRole, self.lbl_binning_selector)
        self.cbb_binning_selector = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_binning_selector.addItems([
            "(default)",
            "Sensor", "Region0", "Region1", "Region2",
        ])
        form_lyt_binning.setWidget(1, QFormLayout.FieldRole, self.cbb_binning_selector)
        self.all_parameters_label.append(self.lbl_binning_selector)
        self.all_parameters_wgt.append(self.cbb_binning_selector)

        # Binning widget: Binning Horizontal Mode
        self.lbl_binning_horizontal_mode = QLabel(self.wgt_binning)
        self.lbl_binning_horizontal_mode.setText("Binning Horizontal Mode")
        form_lyt_binning.setWidget(2, QFormLayout.LabelRole, self.lbl_binning_horizontal_mode)
        self.cbb_binning_horizontal_mode = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_binning_horizontal_mode.addItems([
            "(default)",
            "sum", "average",
        ])
        form_lyt_binning.setWidget(2, QFormLayout.FieldRole, self.cbb_binning_horizontal_mode)
        self.all_parameters_label.append(self.lbl_binning_horizontal_mode)
        self.all_parameters_wgt.append(self.cbb_binning_horizontal_mode)
        
        # Binning widget: Binning Horizontal
        self.lbl_binning_horizontal = QLabel(self.wgt_binning)
        self.lbl_binning_horizontal.setText("Binning Horizontal")
        form_lyt_binning.setWidget(3, QFormLayout.LabelRole, self.lbl_binning_horizontal)
        self.le_binning_horizontal = QLineEdit(self.wgt_binning)
        self.le_binning_horizontal.setValidator(QIntValidator(0, 10))
        self.le_binning_horizontal.setPlaceholderText("Type: integer;  Range: 0 - 10")
        form_lyt_binning.setWidget(3, QFormLayout.FieldRole, self.le_binning_horizontal)
        self.all_parameters_label.append(self.lbl_binning_horizontal)
        self.all_parameters_wgt.append(self.le_binning_horizontal)

        # Binning widget: Binning Vertical Mode
        self.lbl_binning_vertical_mode = QLabel(self.wgt_binning)
        self.lbl_binning_vertical_mode.setText("Binning Vertical Mode")
        form_lyt_binning.setWidget(4, QFormLayout.LabelRole, self.lbl_binning_vertical_mode)
        self.cbb_binning_vertical_mode = CustomComboBox(QFrame(QScrollArea()))
        self.cbb_binning_vertical_mode.addItems([
            "(default)",
            "sum", "average",
        ])
        form_lyt_binning.setWidget(4, QFormLayout.FieldRole, self.cbb_binning_vertical_mode)
        self.all_parameters_label.append(self.lbl_binning_vertical_mode)
        self.all_parameters_wgt.append(self.cbb_binning_vertical_mode)
        
        # Binning widget: Binning Vertical
        self.lbl_binning_vertical = QLabel(self.wgt_binning)
        self.lbl_binning_vertical.setText("Binning Vertical")
        form_lyt_binning.setWidget(5, QFormLayout.LabelRole, self.lbl_binning_vertical)
        self.le_binning_vertical = QLineEdit(self.wgt_binning)
        self.le_binning_vertical.setValidator(QIntValidator(0, 10))
        self.le_binning_vertical.setPlaceholderText("Type: integer;  Range: 0 - 10")
        form_lyt_binning.setWidget(5, QFormLayout.FieldRole, self.le_binning_vertical)
        self.all_parameters_label.append(self.lbl_binning_vertical)
        self.all_parameters_wgt.append(self.le_binning_vertical)

    def init_advanced_transmission(self, index):
        # ---------------------------- Advanced: Transmission ---------------------------- #
        # Transmission label
        self.title_transmission = QLabel(self.wgt_advanced)
        self.title_transmission.setText("Transmission")
        self.grid_lyt_wgt_advanced.addWidget(self.title_transmission, 2 * index, 0, 1, 1)
        self.all_categories_label.append(self.title_transmission)

        # Transmission widget
        self.wgt_transmission = QWidget(self.wgt_advanced)
        form_lyt_transmission = QFormLayout(self.wgt_transmission)
        self.grid_lyt_wgt_advanced.addWidget(self.wgt_transmission, 2 * index + 1, 0, 1, 1)

        # Transmission widget: Blocksize
        self.lbl_blocksize = QLabel(self.wgt_transmission)
        self.lbl_blocksize.setText("Blocksize")
        form_lyt_transmission.setWidget(1, QFormLayout.LabelRole, self.lbl_blocksize)
        self.le_blocksize = QLineEdit(self.wgt_transmission)
        self.le_blocksize.setValidator(QIntValidator(0, 2147483647))
        self.le_blocksize.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_transmission.setWidget(1, QFormLayout.FieldRole, self.le_blocksize)
        self.all_parameters_label.append(self.lbl_blocksize)
        self.all_parameters_wgt.append(self.le_blocksize)

        # Transmission widget: Num Buffers
        self.lbl_num_buffers = QLabel(self.wgt_transmission)
        self.lbl_num_buffers.setText("Num Buffers")
        form_lyt_transmission.setWidget(3, QFormLayout.LabelRole, self.lbl_num_buffers)
        self.le_num_buffers = QLineEdit(self.wgt_transmission)
        self.le_num_buffers.setValidator(QIntValidator(-1, 2147483647))
        self.le_num_buffers.setPlaceholderText("Type: integer;  Range: -1 - 2147483647")
        form_lyt_transmission.setWidget(3, QFormLayout.FieldRole, self.le_num_buffers)
        self.all_parameters_label.append(self.lbl_num_buffers)
        self.all_parameters_wgt.append(self.le_num_buffers)

        # Transmission widget: Packet Delay
        self.lbl_packet_delay = QLabel(self.wgt_transmission)
        self.lbl_packet_delay.setText("Packet Delay")
        form_lyt_transmission.setWidget(4, QFormLayout.LabelRole, self.lbl_packet_delay)
        self.le_packet_delay = QLineEdit(self.wgt_transmission)
        self.le_packet_delay.setValidator(QIntValidator(-1, 2147483647))
        self.le_packet_delay.setPlaceholderText("Type: integer;  Range: -1 - 2147483647")
        form_lyt_transmission.setWidget(4, QFormLayout.FieldRole, self.le_packet_delay)
        self.all_parameters_label.append(self.lbl_packet_delay)
        self.all_parameters_wgt.append(self.le_packet_delay)

        # Transmission widget: Packet Size
        self.lbl_packet_size = QLabel(self.wgt_transmission)
        self.lbl_packet_size.setText("Packet Size")
        form_lyt_transmission.setWidget(5, QFormLayout.LabelRole, self.lbl_packet_size)
        self.le_packet_size = QLineEdit(self.wgt_transmission)
        self.le_packet_size.setValidator(QIntValidator(0, 2147483647))
        self.le_packet_size.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_transmission.setWidget(5, QFormLayout.FieldRole, self.le_packet_size)
        self.all_parameters_label.append(self.lbl_packet_size)
        self.all_parameters_wgt.append(self.le_packet_size)

        # Transmission widget: Throughput Limit
        self.lbl_throughput_limit = QLabel(self.wgt_transmission)
        self.lbl_throughput_limit.setText("Throughput Limit")
        form_lyt_transmission.setWidget(6, QFormLayout.LabelRole, self.lbl_throughput_limit)
        self.le_throughput_limit = QLineEdit(self.wgt_transmission)
        self.le_throughput_limit.setValidator(QIntValidator(0, 2147483647))
        self.le_throughput_limit.setPlaceholderText("Type: integer;  Range: 0 - 2147483647")
        form_lyt_transmission.setWidget(6, QFormLayout.FieldRole, self.le_throughput_limit)
        self.all_parameters_label.append(self.lbl_throughput_limit)
        self.all_parameters_wgt.append(self.le_throughput_limit)

    def set_categories_label_style(self):
        font = QFont()
        font.setPointSize(12)
        font.setItalic(True)
        font.setUnderline(True)

        for category_label in self.all_categories_label:
            category_label.setFont(font)
            category_label.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignVCenter)
    
    def set_parameters_label_style(self):
        for param_label in self.all_parameters_label:
            param_label.setMinimumSize(QSize(180, 0))
            param_label.setMaximumSize(QSize(180, 16777215))

    def set_labels_tooltip(self):
        # Device
        self.lbl_serial.setToolTip(
            "Device's serial number. This string is a unique identifier of the device.")
        self.lbl_pixel_format.setToolTip(
            "Format of the pixels provided by the device. It represents all the information "
            "\nprovided by PixelSize, PixelColorFilter combined in a single feature.")

        # ROI
        self.lbl_width.setToolTip(
            "Width of the image provided by the device (in pixels).")
        self.lbl_height.setToolTip(
            "Height of the image provided by the device (in pixels).")
        self.lbl_offset_x.setToolTip(
            "Horizontal offset from the origin to the region of interest (in pixels).")
        self.lbl_offset_y.setToolTip(
            "Vertical offset from the origin to the region of interest (in pixels).")
        self.lbl_decimation_horizontal.setToolTip(
            "Horizontal sub-sampling of the image.")
        self.lbl_decimation_vertical.setToolTip(
            "Number of vertical photo-sensitive cells to combine together.")
        
        # Analog Control
        self.lbl_gain.setToolTip(
            "Controls the selected gain as an absolute value. This is an amplification "
            "\nfactor applied to video signal. Values are device specific.")
        self.lbl_gain_auto.setToolTip(
            "Sets the automatic gain control (AGC) mode.")
        self.lbl_gain_auto_balance.setToolTip(
            "Sets the mode for automatic gain balancing between the sensor "
            "\ncolor channels or taps.")
        self.lbl_gain_selector.setToolTip(
            "Selects which gain is controlled by the various Gain features. "
            "\nIt's device specific.")
        self.lbl_black_level.setToolTip(
            "Controls the analog black level as an absolute physical value.")
        self.lbl_black_level_auto.setToolTip(
            "Controls the mode for automatic black level adjustment. The exact algorithm "
            "\nused to implement this adjustment is device-specific.")
        self.lbl_black_level_selector.setToolTip(
            "Selects which Black Level is controlled by the various Black Level features.")
        self.lbl_gamma.setToolTip(
            "Controls the gamma correction of pixel intensity.")
        self.lbl_gamma_selector.setToolTip(
            "Select the gamma correction mode.")
        
        # Exposure
        self.lbl_exposure_auto.setToolTip(
            "Sets the automatic exposure mode when ExposureMode is Timed.")
        self.lbl_exposure_mode.setToolTip(
            "Sets the operation mode of the Exposure.")
        self.lbl_exposure_time.setToolTip(
            "Sets the Exposure time (in us) when ExposureMode is Timed and ExposureAuto is Off."
            "\nThis controls the duration where the photosensitive cells are exposed to light.")
        self.lbl_exposure_time_selector.setToolTip(
            "Selects which exposure time is controlled by the ExposureTime feature."
            "\nThis allows for independent control over the exposure components.")
        
        # Balance
        self.lbl_balance_ratio.setToolTip(
            "Controls ratio of the selected color component to a reference color component.")
        self.lbl_balance_ratio_selector.setToolTip(
            "Selects which Balance ratio to control.")
        self.lbl_balance_white_auto.setToolTip(
            "Controls the mode for automatic white balancing between the color channels."
            "\nThe white balancing ratios are automatically adjusted.")
        
        # Acquisition Trigger
        self.lbl_acquisition_mode.setToolTip(
            "Sets the acquisition mode of the device. It defines mainly the number of frames "
            "\nto capture during an acquisition and the way the acquisition stops.")
        self.lbl_trigger_selector.setToolTip(
            "Selects the type of trigger to configure.")
        self.lbl_trigger_source.setToolTip(
            "Specifies the internal signal or physical input Line to use as the trigger source.")
        self.lbl_trigger_activation.setToolTip(
            "Specifies the activation mode of the trigger.")
        self.lbl_trigger_delay.setToolTip(
            "Specifies the delay in microseconds (us) to apply after the trigger reception "
            "\nbefore activating it.")
        self.lbl_trigger_divider.setToolTip(
            "Specifies a division factor for the incoming trigger pulses.")
        self.lbl_trigger_multiplier.setToolTip(
            "Specifies a multiplication factor for the incoming trigger pulses.")
        self.lbl_trigger_overlap.setToolTip(
            "Specifies the type trigger overlap permitted with the previous frame or line.")
        self.lbl_hw_trigger_timeout.setToolTip(
            "Wait timeout (in multiples of 5 secs) to receive frames "
            "\nbefore terminating the application.")
        
        # Binning
        self.lbl_binning_selector.setToolTip(
            "Selects which binning engine is controlled by the BinningHorizontal and "
            "\nBinningVertical features.")
        self.lbl_binning_horizontal.setToolTip(
            "Number of horizontal photo-sensitive cells to combine together."
            "\nThis reduces the horizontal resolution (width) of the image."
            "\nA value of 1 indicates that no horizontal binning is performed by the camera.")
        self.lbl_binning_horizontal_mode.setToolTip(
            "Sets the mode to use to combine horizontal photo-sensitive cells together "
            "\nwhen BinningHorizontal is used.")
        self.lbl_binning_vertical.setToolTip(
            "Number of vertical photo-sensitive cells to combine together."
            "\nThis reduces the vertical resolution (height) of the image."
            "\nA value of 1 indicates that no vertical binning is performed by the camera.")
        self.lbl_binning_vertical_mode.setToolTip(
            "Sets the mode to use to combine vertical photo-sensitive cells together "
            "\nwhen BinningHorizontal is used.")
        
        # Transmission
        self.lbl_blocksize.setToolTip(
            "Size in bytes to read per buffer.")
        self.lbl_frame_rate.setToolTip(
            "Controls the acquisition rate (in Hertz) at which the frames are captured.")
        self.lbl_num_buffers.setToolTip(
            "Number of buffers to output before sending EOS.")
        self.lbl_packet_delay.setToolTip(
            "Controls the delay (in GEV timestamp counter unit) to insert between each packet "
            "\nfor this stream channel. This can be used as a crude flow-control mechanism "
            "\nif the application or the network infrastructure cannot keep up with the packets "
            "\ncoming from the device.")
        self.lbl_packet_size.setToolTip(
            "Specifies the stream packet size, in bytes, to send on the selected channel "
            "\nfor a Transmitter or specifies the maximum packet size supported by a receiver.")
        self.lbl_throughput_limit.setToolTip(
            "Limits the maximum bandwidth (in Bps) of the data that will be streamed out by the device "
            "\non the selected Link. If necessary, delays will be uniformly inserted between transport "
            "\nlayer packets in order to control the peak bandwidth.")

    @staticmethod
    def param_label_to_gst_param(param_label):
        return param_label.text().lower().replace(" ", "-")
    
    def slot_btn_advanced_clicked(self):
        if self.btn_advanced.text().startswith(UNFOLD_CHAR):
            self.wgt_advanced.show()
            self.btn_advanced.setText(FOLD_CHAR + "  Advanced")
        elif self.btn_advanced.text().startswith(FOLD_CHAR):
            self.wgt_advanced.hide()
            self.btn_advanced.setText(UNFOLD_CHAR + "  Advanced")

    def slot_btn_ipconfig_clicked(self):
        proc = sp.run("which mvIPConfigure", shell=True, stdout=sp.PIPE)
        if not proc.stdout.decode():
            QMessageBox.warning(self, "Warning", 
                "The app \"mvIPConfigure\" was not found.\nPlease install it first.",
                QMessageBox.Ok)
            return
        
        # cmd = ["mvIPConfigure"]
        cmd = "echo {} | sudo -S mvIPConfigure".format(self.host_pwd)
        sp.Popen(cmd, shell=True)

    def slot_btn_propview_clicked(self):
        proc = sp.run("which wxPropView", shell=True, stdout=sp.PIPE)
        if not proc.stdout.decode():
            QMessageBox.warning(self, "Warning", 
                "The app \"wxPropView\" was not found.\nPlease install it first.",
                QMessageBox.Ok)
            return
        
        # cmd = ["wxPropView"]
        cmd = "echo {} | sudo -S wxPropView".format(self.host_pwd)
        sp.Popen(cmd, shell=True)
    
    def slot_btn_import_clicked(self):
        # Import the camera config file
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        xml_file = QFileDialog.getOpenFileName(self, "Select camera config file", 
            "/home/{}".format(getpass.getuser()), "XML (*.xml)", options=options)[0]
        if xml_file == "": 
            return

        # Parse params from imported camera config file
        imported_cfg = {}
        with open(xml_file, "r") as f:
            for line in f:
                key = line.split()[0]
                if not key.startswith("<") and not key.startswith("#"):
                    res = line.strip().split()
                    if len(res) == 2:
                        k, v = res
                        # TODO: some keys may exsit multi times, 
                        # so stop searching when firstly meeting
                        # or overwrite the before value ?
                        # if k not in imported_cfg.keys():
                        #     imported_cfg[k] = v
                        imported_cfg[k] = v
        
        # Collect params needed
        filtered_cfg = {}
        for param_lbl, param_mv in CAMERA_PARAMS_MAP.items():
            if isinstance(param_mv, str):
                if param_mv in imported_cfg.keys():
                    filtered_cfg[param_lbl] = imported_cfg[param_mv]
            elif isinstance(param_mv, list):
                for p_mv in param_mv:
                    if p_mv in imported_cfg.keys():
                        filtered_cfg[param_lbl] = imported_cfg[p_mv]
                        break
        
        # Update data to UI
        for param_lbl, param_wgt in zip(self.filtered_parameters_label, self.filtered_parameters_wgt):
            if param_lbl.text() in filtered_cfg.keys():
                if isinstance(param_wgt, QLineEdit):
                    param_wgt.setText(filtered_cfg[param_lbl.text()])
                elif isinstance(param_wgt, CustomComboBox):
                    val = filtered_cfg[param_lbl.text()]
                    
                    # Exception: Pixel Format
                    if param_lbl.text() == "Pixel Format" and val in PIXEL_FORMAT_MAP.keys():
                        param_wgt.setCurrentText(PIXEL_FORMAT_MAP[val])
                    # Exception: Acquisition Mode
                    elif param_lbl.text() == "Acquisition Mode":
                        if val.lower() == "off":
                            param_wgt.setCurrentText("Continuous")
                        else:
                            # TODO: set Singleframe or Multiframe ?
                            param_wgt.setCurrentText("Singleframe")
                    # Exception: Trigger Source
                    elif param_lbl.text() == "Trigger Source":
                        res = re.findall(r"\d+", val)
                        if len(res) == 1:
                            val = val.replace(res[0], "<n>")
                            param_wgt.setCurrentText(val)
                            self.le_trigger_source_number.setText(res[0])
                    # Common cases
                    else:
                        for i in range(param_wgt.count()):
                            item_text = param_wgt.itemText(i)
                            if val.lower() == item_text.lower():
                                param_wgt.setCurrentIndex(i)
                                break
    
    def _generate_gstreamer_basic_cmd(self):
        if self.le_serial.text() == "":
            QMessageBox.warning(self, "Warning", 
                "The paramter, \"Serial\" should not be empty.", QMessageBox.Ok)
            return ""

        # Prepare gstreamer pipeline
        gst_launch_cmd = "gst-launch-1.0 gencamsrc"
        for param_label, param_wgt in zip(self.filtered_parameters_label, self.filtered_parameters_wgt):
            if isinstance(param_wgt, QLineEdit) and param_wgt.text() != "":
                gst_launch_cmd += " {}={}".format(
                    self.param_label_to_gst_param(param_label),
                    param_wgt.text(),
                )
            elif isinstance(param_wgt, CustomComboBox) and param_wgt.currentText() != "(default)":
                gst_launch_cmd += " {}={}".format(
                    self.param_label_to_gst_param(param_label),
                    param_wgt.currentText(),
                )
                # Exception: trigger source
                if param_label.text() == "Trigger Source" and "<n>" in param_wgt.currentText():
                    if self.le_trigger_source_number.text() == "":
                        QMessageBox.warning(self, "Warning", 
                            "The paramter, \"Trigger Source Number\" should not be empty.", QMessageBox.Ok)
                        return
                    else:
                        gst_launch_cmd = gst_launch_cmd.replace("<n>", self.le_trigger_source_number.text())

        return gst_launch_cmd
    
    def slot_btn_tune_clicked(self):
        gst_launch_cmd = self._generate_gstreamer_basic_cmd()
        if gst_launch_cmd == "":
            return
        # Exception: bayerxxxx
        if self.cbb_pixel_format.currentText().startswith("bayer"):
            gst_launch_cmd += " ! bayer2rgb ! ximagesink"
        else:
            gst_launch_cmd += " ! videoconvert ! ximagesink"
        
        gst_header = "echo {} | sudo -E -S ".format(self.host_pwd)
        gst_launch_cmd = gst_header + gst_launch_cmd
        print("GStreamer Tune Pipeline: " + gst_launch_cmd)
        # cmd_list = shlex.split(gst_launch_cmd)
        proc = sp.Popen(gst_launch_cmd, shell=True, stdout=sp.PIPE, stderr=sp.STDOUT)

    def slot_btn_apply_clicked(self):
        if not self.output_wgt:
            QMessageBox.warning(self, "Warning", 
                "No widget to output.", QMessageBox.Ok)
            return
        
        if not hasattr(self.output_wgt, "setText"):
            QMessageBox.warning(self, "Warning", 
                "The widget cannot write text.", QMessageBox.Ok)
            return
        
        gst_launch_cmd = self._generate_gstreamer_basic_cmd()
        if gst_launch_cmd == "":
            return
        gst_launch_cmd = gst_launch_cmd.replace("gst-launch-1.0", "")
        # Exception: bayerxxxx
        if self.cbb_pixel_format.currentText().startswith("bayer"):
            gst_launch_cmd += " ! bayer2rgb ! videoconvert ! video/x-raw,format=BGR ! appsink"
        else:
            gst_launch_cmd += " ! videoconvert ! video/x-raw,format=BGR ! appsink"

        self.output_wgt.setText(gst_launch_cmd.strip())
        if hasattr(self.output_wgt, "setCursorPosition"):
            self.output_wgt.setCursorPosition(0)
        QMessageBox.information(self, "Info", 
            "Applied camera pipeline successfully!", QMessageBox.Ok)
    
    def slots_collector(self):
        self.btn_advanced.clicked.connect(self.slot_btn_advanced_clicked)
        self.btn_ipconfig.clicked.connect(self.slot_btn_ipconfig_clicked)
        self.btn_propview.clicked.connect(self.slot_btn_propview_clicked)
        self.btn_import.clicked.connect(self.slot_btn_import_clicked)
        self.btn_tune.clicked.connect(self.slot_btn_tune_clicked)
        self.btn_apply.clicked.connect(self.slot_btn_apply_clicked)


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    win = CameraTuner()
    win.show()
    sys.exit(app.exec_())
